
#import <Foundation/Foundation.h>

#include <Liberty3D/Engine/XML/Parser.h>

using namespace Liberty::Engine::Xml;

std::shared_ptr<INode> Parser::parse(std::ios& i) {
    return parse(i.rdbuf());
}

namespace Liberty { namespace Engine { namespace Xml {
    class Node : public INode {
    protected:
        NSXMLNode* _node;
        
    public:
        Node(NSXMLNode* node);
        virtual ~Node();
        
    public:
        virtual Type        type() const;
        virtual std::string data() const;
        
        virtual bool        hasAttribute(const std::string&) const;
        virtual std::string attribute(const std::string& name, const std::string& defaultValue) const;
        virtual void        setAttribute(const std::string& name, const std::string& v);
        
        virtual void                   childs(std::vector< std::shared_ptr<INode> >&) const;
        virtual std::shared_ptr<INode> firstChild() const;
        virtual std::shared_ptr<INode> firstChildElement(const std::string&) const;
        virtual std::shared_ptr<INode> nextSiblingElement(const std::string&) const;
        
        virtual std::shared_ptr<INode> addElement(const std::string& name, const std::shared_ptr<INode>& before);
        virtual std::shared_ptr<INode> addText   (const std::string& data, const std::shared_ptr<INode>& before);
        
        virtual void removeChild(const std::shared_ptr<INode>& child);
        
        virtual std::string toString() const;
        virtual void toXml(const std::string& fileName) const;
        virtual void toXml(std::ios&) const;
        virtual void toXml(std::streambuf* buf) const;
        
    protected:
        NSUInteger findNode(const std::shared_ptr<INode>&) const;
        void        addNode(NSXMLNode* node, const std::shared_ptr<INode>& before);
    };
}}}

Node::Node(NSXMLNode* node) : _node(node) {
}

Node::~Node() {
}

Node::Type Node::type() const {
    switch ([_node kind]) {
        case NSXMLDocumentKind: return DOC;
        case NSXMLElementKind:  return ELEMENT;
        case NSXMLTextKind:     return TEXT;
        default:                return UNKNOWN;
    }
}

std::string Node::data() const {
    @autoreleasepool {
        switch ([_node kind]) {
            case NSXMLElementKind:  return [[_node name] UTF8String];
            default:                return [[_node stringValue] UTF8String];
        }
    }
}

bool Node::hasAttribute(const std::string& s) const {
    @autoreleasepool {
        if ([_node kind] == NSXMLElementKind) {
            return ([(NSXMLElement*)_node attributeForName:[NSString stringWithUTF8String:s.c_str()]]) != nil;
        }
        
        return false;
    }
}

std::string Node::attribute(const std::string& name, const std::string& defaultValue) const {
    @autoreleasepool {
        if ([_node kind] == NSXMLElementKind) {
            NSXMLNode* attr = [(NSXMLElement*)_node attributeForName:[NSString stringWithUTF8String:name.c_str()]];
            
            if (attr) {
                return [[attr stringValue] UTF8String];
            }
        }
        
        return defaultValue;
    }
}

void Node::setAttribute(const std::string& name, const std::string& value) {
    @autoreleasepool {
        if ([_node kind] == NSXMLElementKind) {
            NSString* nameNS = [NSString stringWithUTF8String:name.c_str()];
            
            if (value.length() == 0) {
                [(NSXMLElement*)_node removeAttributeForName:nameNS];
            }
            else {
                NSXMLNode* attr    = [(NSXMLElement*)_node attributeForName:nameNS];
                NSString*  valueNS = [NSString stringWithUTF8String:value.c_str()];
                
                if (attr) {
                    [attr setStringValue:valueNS resolvingEntities:NO];
                }
                else {
                    [(NSXMLElement*)_node addAttribute:[NSXMLNode attributeWithName:nameNS stringValue:valueNS]];
                }
            }
        }
    }
}

void Node::childs(std::vector< std::shared_ptr<INode> >& nodes) const {
    nodes.clear();

    @autoreleasepool {
        NSArray* a = [_node children];
            
        for (NSXMLNode* node in a) {
            nodes.push_back(std::shared_ptr<INode>(new Node(node)));
        }
    }
}

std::shared_ptr<INode> Node::firstChild() const {
    @autoreleasepool {
        NSXMLNode* f = [_node childAtIndex:0];
        
        if (f != nil) {
            return std::shared_ptr<INode>(new Node(f));
        }
        
        return std::shared_ptr<INode>();
    }
}

std::shared_ptr<INode> Node::firstChildElement(const std::string& name) const {
    @autoreleasepool {
        if ([_node childCount] == 0) {
            return std::shared_ptr<INode>();
        }
        
        NSXMLNode* next = [_node childAtIndex:0];
        NSString*  nextName = name.length()? [NSString stringWithUTF8String:name.c_str()]: nil;
        
        while (next != nil) {
            if ([next kind] == NSXMLElementKind) {
                if (nextName) {
                    if ([[next name] isEqualToString:nextName]) {
                        return std::shared_ptr<INode>(new Node(next));
                    }
                }
                else {
                    return std::shared_ptr<INode>(new Node(next));
                }
            }
            
            next = [next nextSibling];
        }
    
        return std::shared_ptr<INode>();
    }
}

std::shared_ptr<INode> Node::nextSiblingElement(const std::string& name) const {
    @autoreleasepool {
        NSXMLNode* next = [_node nextSibling];
        NSString*  nextName = name.length()? [NSString stringWithUTF8String:name.c_str()]: nil;
            
        while (next) {
            if ([next kind] == NSXMLElementKind) {
                if (nextName) {
                    if ([[next name] isEqualToString:nextName]) {
                        return std::shared_ptr<INode>(new Node(next));
                    }
                }
                else {
                    return std::shared_ptr<INode>(new Node(next));
                }
            }

            next = [next nextSibling];
        }
        
        return std::shared_ptr<INode>();
    }
}

NSUInteger Node::findNode(const std::shared_ptr<INode>& n) const {
    if (!n) {
        return [_node childCount];
    }
    
    NSUInteger index  = 0;
    NSXMLNode* node   = [_node childAtIndex:0];
    NSXMLNode* nodeNS = std::dynamic_pointer_cast<Node>(n)->_node;
    
    while (node != nil) {
        if (nodeNS == node) {
            break;
        }
        
        node = [node nextSibling];
        index++;
    }
    
    return index;
}

void Node::addNode(NSXMLNode* nn, const std::shared_ptr<INode>& before) {
    if (!before) {
        [(NSXMLElement*)_node addChild:nn];
    }
    else {
        [(NSXMLElement*)_node insertChild:nn atIndex:findNode(before)];
    }
}

std::shared_ptr<INode> Node::addElement(const std::string& name, const std::shared_ptr<INode>& before) {
    @autoreleasepool {
        if ([_node kind] == NSXMLElementKind) {
            NSXMLElement* element = [NSXMLNode elementWithName:[NSString stringWithUTF8String:name.c_str()]];
            
            addNode(element, before);
            return std::shared_ptr<INode>(new Node(element));
        }
        
        return std::shared_ptr<INode>();
    }
}

std::shared_ptr<INode> Node::addText(const std::string& data, const std::shared_ptr<INode>& before) {
    @autoreleasepool {
        NSXMLNode* nn;
        
        if (containsWhitespaces(data)) {
            NSXMLNode* nn = [[NSXMLNode alloc] initWithKind:NSXMLTextKind  options:NSXMLNodeIsCDATA];
            [nn setStringValue:[NSString stringWithUTF8String:data.c_str()]];
        }
        else {
            nn = [NSXMLNode textWithStringValue:[NSString stringWithUTF8String:data.c_str()]];
        }
        
        addNode(nn, before);
        return std::shared_ptr<INode>(new Node(nn));
    }
}

void Node::removeChild(const std::shared_ptr<INode>& child) {
    @autoreleasepool {
        if ([_node kind] == NSXMLElementKind) {
            [(NSXMLElement*)_node removeChildAtIndex:findNode(child)];
        }
    }
}

std::string Node::toString() const {
    @autoreleasepool {
        return [[_node XMLStringWithOptions:NSXMLNodePrettyPrint] UTF8String];
    }
}

void Node::toXml(const std::string& fileName) const {
    @autoreleasepool {
        [[_node XMLStringWithOptions:NSXMLNodePrettyPrint] writeToFile:[NSString stringWithUTF8String:fileName.c_str()]
                                                            atomically:NO
                                                              encoding:NSUTF8StringEncoding
                                                                 error:nil];
    }
}

void Node::toXml(std::ios& s) const {
    toXml(s.rdbuf());
}

void Node::toXml(std::streambuf* buf) const {
    @autoreleasepool {
        NSData* data = [[_node XMLStringWithOptions:NSXMLNodePrettyPrint] dataUsingEncoding:NSUTF8StringEncoding];
        
        buf->sputn((const char*)[data bytes], [data length]);
    }
}

std::shared_ptr<INode> Parser::parse(const std::string& fileName) {
    @autoreleasepool {
        NSXMLDocument* doc = [[NSXMLDocument alloc] initWithContentsOfURL:[NSURL fileURLWithPath:[NSString stringWithUTF8String:fileName.c_str()]]
                                                                  options:NSXMLNodePreserveCDATA
                                                                    error:nil];
        
        return std::shared_ptr<INode>(new Node(doc));
    }
}

std::shared_ptr<INode> Parser::parseString(const std::string& xmlData) {
    @autoreleasepool {
        NSXMLDocument* doc = [[NSXMLDocument alloc] initWithXMLString:[NSString stringWithUTF8String:xmlData.c_str()]
                                                              options:NSXMLNodePreserveCDATA
                                                                error:nil];
        
        return std::shared_ptr<INode>(new Node(doc));
    }
}

std::shared_ptr<INode> Parser::parse(std::streambuf* buf) {
    std::streampos pos = buf->pubseekpos(0);
    std::streampos end = buf->pubseekoff(0, std::ios::end);
    
    buf->pubseekoff(pos, std::ios::beg);
    
    size_t dataLength = end - pos;
    void*  data       = malloc(dataLength);
    
    buf->sgetn((char*)data, dataLength);
    
    @autoreleasepool {
        NSData*        dataNS = [NSData dataWithBytesNoCopy:data length:dataLength freeWhenDone:YES];
        NSXMLDocument* doc    = [[NSXMLDocument alloc] initWithData:dataNS
                                                            options:NSXMLNodePreserveCDATA
                                                              error:nil];
        
        return std::shared_ptr<INode>(new Node(doc));
    }
}
